<?php
/**
 * API安全性质业务方法,前提必须登录以后的
 * 单用户多设备登录，不允许单用户单一设备登录
 */
class SecurityService extends TP_Service
{
    protected $arr = []; //表头参数

    protected $expire_long = 0;
    protected $expire_short = 0;

    public function __construct()
    {
        parent::__construct();
        $this->load->helper('jwt');
    }

    /**
     * [init description]
     * @Description 初始化方法，带参数
     * @Author      FengPQ
     * @DateTime    2020-07-26T23:24:25+0800
     * @param       boolean                  $isLogin [description]
     * @param       string                   $uuid    [description]
     * @return      [type]                            [description]
     */
    private function init($isLogin = false, $uuid = "")
    {
        // 获取token、timestamps等表头参数，并做第一步参数校验
        $this->dealParam($isLogin, $uuid);

        //获取accesstoken有效期，和longtoken时长
        $this->expire_short = intval((new DeviceType())->getByValue($this->arr['source'])[TPEnum::Append1]);
        $this->expire_long = intval((new DeviceType())->getByValue($this->arr['source'])[TPEnum::Append2]);
        //不同设备的预操作
        $this->dealByDevice();
    }

    /**
     * [generateTokens description]
     * @Description 生成一个全新token，使用在登录场景
     *     登录场景不走dealParam，uuid是空的，需要登录成功传进来
     * @Author      FengPQ
     * @DateTime    2020-05-20T13:32:00+0800
     * @return      [type]                   [description]
     */
    public function loginAccess($uuid)
    {
        $this->init(true, $uuid);
        $accessToken = $this->getAccessToken();
        //通过redis新增longtoken
        $this->cache->redis->set(redisLongToken($this->arr['uuid'], $this->arr['source']), $accessToken, $this->expire_long);
    }

    /**
     * [removeTokens description]
     * @Description 删除token，用在注销,修改密码等场景
     * @Author      FengPQ
     * @DateTime    2020-05-22T23:27:05+0800
     */
    public function removeTokens()
    {
        $this->init();
        $this->cache->redis->delete(redisLongToken($this->arr['uuid'], $this->arr['source']));
    }

    /**
     * [checkTokens description]
     * @Description 验证token，重新续期access token 和 refresn token,用在登陆后每次接口访问中
     * @Author      FengPQ
     * @DateTime    2020-05-20T13:32:22+0800
     */
    public function checkTokens()
    {
        $this->init();
        //预先在临时redis中查是否有临时的,有临时，并且是对应token，直接通过，不做校验
        if ($this->cache->redis->exists(redisTempLongToken($this->arr['uuid'], $this->arr['source'])) && $this->cache->redis->get($this->arr['uuid'], $this->arr['source']) == $this->arr['token']) {
            return true;
        }

        //第一步：验证redis缓存中longtoken是否存在,不存在，则token过期
        if (!$this->cache->redis->exists(redisLongToken($this->arr['uuid'], $this->arr['source']))) {
            throw new \TPException(ErrCode::ErrCode_10103);
        }

        //第二步：验证redis缓存中longtoken是否存在,不存在，则无效token
        if ($this->cache->redis->get(redisLongToken($this->arr['uuid'], $this->arr['source']) != $this->arr['token'])) {
            throw new \TPException(ErrCode::ErrCode_10104);
        }

        //第三步：验证longtoken是否需要延期
        log_json($this->cache->redis);
        $long_token_limit = $this->cache->redis->ttl(redisLongToken($this->arr['uuid'], $this->arr['source']));
        log_info($long_token_limit);
        //判断longToken是否快过期，即是否小于2倍accesstoken，小了就自动续期
        log_info(redisLongToken($this->arr['uuid'], $this->arr['source']));
        log_info($this->expire_long);
        if ($long_token_limit <= 2 * $this->expire_short) {
            log_json($this->cache->redis);
            // $this->cache->redis->expire(redisLongToken($this->arr['uuid'], $this->arr['source']), $this->expire_long);
            $this->cache->redis->expire('123456', 80);
        }
        log_info(redisLongToken($this->arr['uuid'], $this->arr['source']));

        //第四步：验证accesstoken是否有效
        $this->checkAccessToken();
    }

    /**
     * [checkAccessToken description]
     * @Description accesstoken验证
     * @Author      FengPQ
     * @DateTime    2020-05-20T21:00:38+0800
     * @return      [type]                   [description]
     */
    private function checkAccessToken()
    {
        $accesstoken = $this->arr['token'];

        $getPayload_token = Jwt::verifyToken($accesstoken);

        if (false == $getPayload_token) {
            //token无效，则先清除当前redis，再直接抛出错误去，需要重新登录
            $this->cache->redis->delete(redisLongToken($this->arr['uuid'], $this->arr['source']));
            throw new \TPException(ErrCode::ErrCode_10102);
        } else if (1 == $getPayload_token) {
            //accesstoken失效，需要重新续期
            //最早第一步预先做了long token的有效期，故有效
            $new_access_token = $this->getAccessToken();
            //新的access加入redis
            $this->cache->redis->set(redisLongToken($this->arr['uuid'], $this->arr['source']), $new_access_token, $this->expire_short);
            //老的access加入到expire减少
            $this->cache->redis->set(redisTempLongToken($this->arr['uuid'], $this->arr['source']), $accesstoken, BACKUP_TOKEN_EXPIRE);
        }
    }

    /**
     * [dealByDevice description] -暂时不用,代码有问题
     * @Description 不同设备的预操作;单设备下，清空缓存
     * @Author      FengPQ
     * @DateTime    2020-05-23T00:02:41+0800
     * @return      [type]                   [description]
     */
    private function dealByDevice()
    {
        /**
        //如果单设备登录，使用一个固定的token，先删除原来的,在新增新的token；多设备登录不限制操作
        $_datas = $this->cache->redis->keys(redisLongToken($this->arr['uuid'], $this->arr['token'], $this->arr['source'], true));
        //单设备情况下，删除多余残留的所有缓存
        foreach ($_datas as $key => $v) {
        $this->cache->redis->delete($key);
        }

        //通过redis新增longtoken
        $this->cache->redis->set(redisLongToken($this->arr['uuid'], $this->arr['token'], $this->arr['source']), $this->expire_long);
         **/
    }

    /**
     * [getAccessToken description]
     * @Description 获取accessTOken
     * @Author      FengPQ
     * @DateTime    2020-05-20T13:23:43+0800
     * @return      [type]                                 [description]
     */
    private function getAccessToken()
    {
        return $this->getToken($this->expire_short);
    }

    /**
     * [getToken description]
     * @Description JWT生成
     * @Author      FengPQ
     * @DateTime    2020-05-20T02:32:28+0800
     * @return      [type]                   [description]
     */
    private function getToken($device_token_expire)
    {
        $payload = array(
            'iss' => md5(TP),
            'iat' => time(),
            'exp' => time() + $device_token_expire,
            'nbf' => time(),
            'jti' => md5(uniqid('JWT') . time()),
            'uuid' => $this->arr['uuid'],
        );

        $jwt_token = Jwt::getToken($payload);
        return $jwt_token;
    }

    /**
     * [dealParam ] 参数处理
     * @Description 参数处理及校验
     * @Author      FengPQ
     * @DateTime    2020-05-14T17:00:10+0800
     */
    private function dealParam($isLogin, $uuid)
    {
        //接受header参数--系统参数[uuid、source、timestamp、token、timestamp]
        $this->arr = $_SESSION[SESS_HEADER];

        if ($isLogin && $uuid != "") {
            //如果是登录，用获取的uuid覆盖当前参数
            $this->arr['uuid'] = $uuid;
            //将uuid放到session全局变量中
            $_SESSION[SESS_HEADER]['uuid'] = $this->arr['uuid'];
        }

        //session中赋值用uuid，为了方便整个程序中使用当前用户
        $_SESSION[SESS_UUID] = $this->arr['uuid'];

        //参数是否存在
        api_params_check($this->arr['timestamp'], $this->arr['source'], $this->arr['uuid']);

        //check时，uuid和token不能为空
        if (!$isLogin) {
            api_params_check($this->arr['token']);
        }

        //服务器时间与客户端时间校验，不能相差1小时
        $timestamp_line = intval($this->arr['timestamp']) + intval(CS_EXPIRE_SECOND);

        if ($timestamp_line < time()) {
            throw new \TPException(ErrCode::ErrCode_10101);
        }

    }

}
